package com.wjk.kylin.mall.gateway.authorization;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.wjk.kylin.mall.common.core.constant.Constants;
import com.wjk.kylin.mall.common.core.exception.BaseException;
import com.wjk.kylin.mall.common.redis.utils.RedisUtil;
import com.wjk.kylin.mall.gateway.feign.FeignHolder;
import com.wjk.kylin.mall.gateway.util.SysContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import reactor.core.publisher.Mono;

import java.util.*;
import java.util.concurrent.Future;

/**
 * 鉴权管理器
 */
@Slf4j
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private FeignHolder feignHolder;


    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        ServerHttpRequest request = authorizationContext.getExchange().getRequest();
        String traceId = request.getHeaders().getFirst(Constants.TRACE_ID_KEY);
        if (StringUtils.isBlank(traceId)) {
            traceId = IdUtil.simpleUUID();
            log.info("header traceId is null create new traceId:{}", traceId);
            //头信息设置traceId
            request.mutate().header(Constants.TRACE_ID_KEY, traceId).build();
        }
        MDC.put(Constants.TRACE_ID_KEY, traceId);
        SysContext.setTraceId(traceId);

        //1.对应跨域的预检请求直接放行
        if (request.getMethod() == HttpMethod.OPTIONS) {
            return Mono.just(new AuthorizationDecision(true));
        }
        PathMatcher pathMatcher = new AntPathMatcher();
        //RESTFul接口权限设计 @link https://www.cnblogs.com/haoxianrui/p/14961707.html
        String method = request.getMethodValue();
        String path = request.getURI().getPath();
        String restfulPath = method + ":" + path;
        log.info("鉴权管理器-访问路径：[{}]", restfulPath);

        //如果token以"bearer "为前缀，到此方法里说明JWT有效即已认证，其他前缀的token则拦截
        String token = request.getHeaders().getFirst(Constants.TOKEN_HEADER);
        //1.token为空禁止访问
        if (StringUtils.isBlank(token)) {
            return Mono.just(new AuthorizationDecision(false));
        }
        //2.非token以"bearer "为前缀 ,空禁止访问
        if (!StrUtil.startWithIgnoreCase(token, Constants.TOKEN_TYPE_BEARER)) {
            return Mono.just(new AuthorizationDecision(false));
        }
        //3.非管理端路径无需鉴权直接放行
        if (!pathMatcher.match(Constants.ADMIN_URL_PATTERN, path)) {
            return Mono.just(new AuthorizationDecision(true));
        }

        /**
         * 只能异步调用
         */
        List<Long> roleIdList = new ArrayList<>();
        Future<List<Long>> future = feignHolder.getRoleIdByComponent(path);
        try {
            roleIdList = future.get();
        } catch (Exception e) {
            log.error("鉴权管理器-访问路径：[{}],调用Feign接口异常,e:", path, e);
            throw new BaseException("鉴权异常，请重新尝试！");
        }

        /**
         * 鉴权开始
         * 缓存取 [URL权限-角色集合] 规则数据
         * urlPermRolesRules = [{'key':'GET:/api/v1/users/*','value':['ADMIN','TEST']},...]
         */
        Map<Object, Object> urlPermRolesRules = redisUtil.hmget(Constants.URL_PERM_ROLES_KEY);

        // 根据请求路径判断有访问权限的角色列表
        List<String> authorizedRoles = new ArrayList<>(); // 拥有访问权限的角色
        boolean requireCheck = false; // 是否需要鉴权，默认未设置拦截规则不需鉴权

        for (Map.Entry<Object, Object> permRoles : urlPermRolesRules.entrySet()) {
            String perm = (String) permRoles.getKey();
            if (pathMatcher.match(perm, restfulPath)) {
                List<String> roles = Convert.toList(String.class, permRoles.getValue());
                authorizedRoles.addAll(Convert.toList(String.class, roles));
                if (!requireCheck) {
                    requireCheck = true;
                }
            }
        }
        if (!requireCheck) {
            return Mono.just(new AuthorizationDecision(true));
        }

        // 判断JWT中携带的用户角色是否有权限访问
        return mono
                .filter(Authentication::isAuthenticated)
                .flatMapIterable(Authentication::getAuthorities)
                .map(GrantedAuthority::getAuthority)
                .any(authority -> {
                    String roleCode = authority.substring(Constants.AUTHORITY_PREFIX.length()); // 用户的角色
                    // 5. authority是请求用户的角色(格式:ROLE_{authority})，authorities是请求资源所需要角色的集合
                    log.info("鉴权管理器-访问路径：[{}]", restfulPath);
                    log.info("鉴权管理器-用户角色authority：[{}]", authority);
                    log.info("鉴权管理器-资源需要权限authorizedRoles：[{}]", authorizedRoles);
                    if (Constants.ROOT_ROLE_CODE.equals(roleCode)) {
                        return true; // 如果是超级管理员则放行
                    }
                    return CollectionUtil.isNotEmpty(authorizedRoles) && authorizedRoles.contains(roleCode);
                })
                .map(AuthorizationDecision::new)
                .defaultIfEmpty(new AuthorizationDecision(false));
    }
}
